# !/usr/bin/env python3
# coding=utf-8
"""
读取通达信各板块转换后的XLSX格式数据的类
读取通达信软件下载的数据通用模块
注意:
    1、Txt格式的数据可以直接读取
    2、Xls格式的数据需要转换为Xlsx格式的数据后再读取
作者: KydGroups
版本: 0.0.0
完成日期:
修改记录:
"""
import os
import openpyxl
from KydPyPub.Sys import TypeConvert

class KydTdxData():
    """
    读取通达信各板块转换后的XLSX格式数据的类,会对数据进行处理
    """
    def __init__(self):
        """
        :DESCRIPTION:
            初始化函数
        :PARAMS:
            nothing
        :RETURNS:
            nothing
        """
        self.__listUsedEncoding = ['gbk', 'GBK',
                                   'gb2312', 'gb-2312', 'GB2312', 'GB-2312',
                                   'utf8', 'utf-8', 'UTF8' , 'UTF-8',
                                   'gb18030', 'GB18030', 'gb-18030', 'GB-18030'] #可用的字符编码
        self.__listQuarter = ['㈠', '㈡', '㈢', '㈣'] #季度值列表
        self.__strEncoding = 'gb2312' #默认字符编码
        self.__strFilePath = '' #数据文件路径
        self.__strFileName = '' #数据文件的名称,不包含目录的名字
        self.__strDefaultDate = '1991-01-01' #缺省默认日期
        self.__dictConvertFields = dict() #字段转换定义字典
        self.__bXlsxFile = True #标记文件为XLSX文件,这个值用于标准化处理
        self.__bNotEsta = False #是否添加没上市的股票,默认是不返回
        self.__listData = [] #保存读取数据的列表
        self.__listFields = [] #保存字段名称的列表
        self.__InitialConvertFields() #初始化转换字段定义


    def __InitialConvertFields(self):
        """
        :DESCRIPTION:
            初始化表头与字段转换定义
        :PARAMS:
            nothing
        :RETURNS:
            nothing
        """
        self.__AddSingleConvertField(['StockCode', '代码', 'STR', None, None])
        self.__AddSingleConvertField(['StockName', '名称', 'STR', None, None])
        self.__AddSingleConvertField(['Industry', '细分行业', 'STR', None, None])
        self.__AddSingleConvertField(['Region', '地区', 'STR', None, None])
        self.__AddSingleConvertField(['EstaDate', '上市日期', 'DATE', '--', '2023-01-01'])
        self.__AddSingleConvertField(['FinaDate', '财务更新', 'DATE', '--', '2023-01-01'])
        self.__AddSingleConvertField(['DynamicPE','市盈(动)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['CircEquity','流通股(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['CircCapital','流通市值','FLOAT','--',0.00])
        self.__AddSingleConvertField(['MarketCapital','总市值','FLOAT','--',0.00])
        self.__AddSingleConvertField(['StaticPE','市盈(静)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['TotalEquity','总股本(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['TotalAssets','总资产(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['NetAssets','净资产(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['MinorityEquity','少数股权(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['DebtAssetRatio','资产负债率%','FLOAT','--',0.00])
        self.__AddSingleConvertField(['CurrentAssets','流动资产(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['FixedAssets','固定资产(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['IntangibleAssets','无形资产(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['CurrentLiabilities','流动负债(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['Inventory','存货(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['AccountsReceivable','应收账款(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['CapitalSurplus','资本公积金(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['OperatingRevenue','营业收入(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['OperatingCosts','营业成本(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['OperatingProfit','营业利润(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['INVIncome','投资收益(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['TotalProfit','利润总额(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['AfterTaxProfit','税后利润(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['NetProfit','净利润(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['DeNonNetProfit','扣非净利润(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['UndistProfit','未分利润(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['OperatingCashFlow','经营现金流(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['TotalCashFlow','总现金流(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['NumOfShare','股东人数','INT','--',0])
        self.__AddSingleConvertField(['ProfitYOYRatio','利润同比%','FLOAT','--',0.00])
        self.__AddSingleConvertField(['RevenueYOYRatio','收入同比%','FLOAT','--',0.00])
        self.__AddSingleConvertField(['PBRatio','市净率','FLOAT','--',0.00])
        self.__AddSingleConvertField(['PCFRatio','市现率','FLOAT','--',0.00])
        self.__AddSingleConvertField(['PSRatio','市销率','FLOAT','--',0.00])
        self.__AddSingleConvertField(['DividendYield','股息率%','FLOAT','--',0.00])
        self.__AddSingleConvertField(['EachIncome','每股收益','FLOAT','--','0.00'])
        self.__AddSingleConvertField(['EachNetAssets','每股净资','FLOAT','--',0.00])
        self.__AddSingleConvertField(['EachAccFund','每股公积','FLOAT','--',0.00])
        self.__AddSingleConvertField(['EachUndistProfit','每股未分配','FLOAT','--',0.00])
        self.__AddSingleConvertField(['EachCashFlow','每股现金流','FLOAT','--',0.00])
        self.__AddSingleConvertField(['EquityRatio','权益比%','FLOAT','--',0.00])
        self.__AddSingleConvertField(['RERatio','净益率%','FLOAT','--',0.00])
        self.__AddSingleConvertField(['CrossProfitRatio','毛利率%','FLOAT','--',0.00])
        self.__AddSingleConvertField(['OperatProfitRatio','营业利润率%','FLOAT','--',0.00])
        self.__AddSingleConvertField(['NetProfitRatio','净利润率%','FLOAT','--',0.00])
        self.__AddSingleConvertField(['RDExpenses','研发费用(亿)','FLOAT','--',0.00])
        self.__AddSingleConvertField(['IncreaseRate','涨幅%','FLOAT','--',0.00])
        self.__AddSingleConvertField(['CurAMP','振幅%','FLOAT','--',0.00])
        self.__AddSingleConvertField(['CurAmount','总金额','FLOAT','--',0.00])
        self.__AddSingleConvertField(['OpenPrice','今开','FLOAT','--',0.00])
        self.__AddSingleConvertField(['HighPrice','最高','FLOAT','--',0.00])
        self.__AddSingleConvertField(['LowPrice','最低','FLOAT','--',0.00])
        self.__AddSingleConvertField(['ClosePrice','现价','FLOAT','--',0.00])
        self.__AddSingleConvertField(['CurVolume','总量','INT','--',0])
        self.__AddSingleConvertField(['TurnoverRate','换手%','FLOAT','--',0.00])
        self.__AddSingleConvertField(['IncreaseSpeed','涨速%','FLOAT','--',0.00])
        self.__AddSingleConvertField(['VolumeRate','量比','FLOAT','--',0.00])
        self.__AddSingleConvertField(['IERate','内外比','FLOAT','--',0.00])


    def __AddSingleConvertField(self, listDef:list=[]):
        """
        :DESCRIPTION:
            添加单个字段转换定义
        :PARAMS:
            listDef: 列表形式的定义
        :RETURNS:
            nothing
        """
        if not listDef:
            return False
        if listDef[0] in self.__dictConvertFields.keys():#如果定义已经在
            return False
        else:
            dictFields = dict()
            dictFields['Describe'] = listDef[1] #表头中文描述
            dictFields['Type'] = listDef[2] #字段数值类型
            dictFields['ErrorValue'] = listDef[3] #错误值类型
            dictFields['DefaultValue'] = listDef[4] #如果出现错误值时,使用的默认值,如果为None则出现错误时会中断并显示对应的值
            dictFields['Pos'] = -1 #字段在分隔字符串数组中的位置,这里是默认值
            self.__dictConvertFields[listDef[0]] = dictFields #添加一个字段定义
            self.__listFields.append(listDef[0]) #添加一个字段名称
            return True


    def __SetEncoding(self, encoding:str='gb2312'):
        """
        :DESCRIPTION:
            设置字符编码格式
        :PARAMS:
            encoding: 字符编码格式(默认gb2312)
        :RETURNS:
            True:设置成功 False:设置失败
        """
        if not encoding: #如果没有传入参数
            strError = TypeConvert.StrToColorStr("KydClsTdxBoardTxt---->>SetEncoding函数没有传入参数!", 'alarm')
            print(strError)
            self.__strEncoding = 'gb2312' #使用默认值
            return False
        if encoding not in self.__listUsedEncoding:
            strError = TypeConvert.StrToColorStr("KydClsTdxBoardTxt---->>SetEncoding函数传入参数 %s ,不是合规的字符编码!" % encoding, 'alarm')
            print(strError)
            self.__strEncoding = 'gb2312' #使用默认值
            return False
        self.__strEncoding = encoding
        return True


    def ReadFile(self, strPath:str='', bForce:bool=False, encoding:str='gb2312', bNotEsta:bool=False):
        """
        :DESCRIPTION:
            读取文件中的数据,可以是xlsx,也可以是.txt文件
        :PARAMS:
            strPath: 文件的路径
            bForce: 如果已经有数据是否删除数据强制读取, False(默认):不强制读取  True:强制读取
            encoding: 字符编码,必须在设置的选项中
            bNotEsta: 是否添加没有上市的股票,False(默认):不添加 True:添加
        :RETURNS:
            False:读取失败 True:读取成功
        """
        if not strPath: #如果没有传入参数
            strError = TypeConvert.StrToColorStr("KydTdxData---->>ReadXLSX 没有传入参数!")
            print(strError)
            return False
        else: #传入了参数,则判断文件是否存在
            self.__strFileName = os.path.split(strPath)[1]
            if not os.path.exists(strPath): #如果文件不存在
                strError = TypeConvert.StrToColorStr("KydTdxData---->>ReadXLSX 文件%s 不存在!" % self.__strFileName)
                print(strError)
                return False
        if len(self.__listData) >= 1: #如果有数据
            if not bForce: #如果不强制读取
                return False
            else:
                self.__listData.clear() #清空数据
        self.__strFilePath = strPath #设置类中保存的文件路径
        self.__bNotEsta = bNotEsta #设置是否添加未上市股票
        self.__SetEncoding(encoding) #设置字符编码
        for key in self.__listFields: #重置值位置信息
            self.__dictConvertFields[key]['Pos'] = -1
        if strPath[-5:].upper() == ".XLSX": #如果是xlsx文件
            self.__bXlsxFile = True #表示是XLSX文件
            return self.__ReadXLSX()
        elif strPath[-4:].upper() == ".TXT": #如果是txt文件
            self.__bXlsxFile = False #表示不是XLSX文件
            return self.__ReadTXT()
        else: #其它格式的文件不支持
            strError = TypeConvert.StrToColorStr("KydTdxData---->>ReadXLSX 文件%s 类型不支持!" % self.__strFileName)
            print(strError)
            return False
            

    def __ReadXLSX(self):
        """
        :DESCRIPTION:
            读取xlsx文件中的数据,不需要检查参数,上位函数已经检查
        :PARAMS:
            nothing
        :RETURNS:
            True:读取成功 False:读取失败
        """
        try:
            print("正在读取文件%s 需要一些时间,请等待.........." % self.__strFileName)
            inwb = openpyxl.load_workbook(self.__strFilePath) #调用openpyxl库读取xlsx数据
            if not inwb: #如果读取失败
                print( TypeConvert.StrToColorStr("读取%s文件失败!" % self.__strFileName, 'alarm') )
                return False
            else:
                sheet = inwb[inwb.sheetnames[0]] #获取第一个表格
                nRows = sheet.max_row #获取表格行数
                nCols = sheet.max_column #获取表格列数
                print( TypeConvert.StrToColorStr("读取%s文件成功, 共有%d行, %d列" % (self.__strFileName, nRows, nCols), 'green') ) #显示读取基本结果
                listRows = list(sheet.rows) #将数据转换为列表格式
                listRead = list() #保存转换后数据的列表
                for item in listRows: #将读取的xlsx数据转换为列表形式
                    singleRow = [cell.value for cell in item]
                    listRead.append(singleRow)

                if not self.__SetConvertFieldsPos(listRead[0]): #如果设置转换字段对应位置不成功
                    print( TypeConvert.StrToColorStr("设置转换字段位置不成功!", 'alarm') )
                    return False
                else:
                    dictEstaDate = self.__dictConvertFields['EstaDate'] #获取上市日期的转换定义
                    for n in range(1, nRows): #循环处理,第一行表头和最后一行出处不需要处理
                        singleRow = listRead[n] #获取当前行的单元格数据
                        if not self.__bNotEsta: #如果不添加未上市的股票
                            if isinstance(singleRow[dictEstaDate['Pos']], str): #如果上市日期的值与错误值相同
                                continue #跳过这条数据
                        dictLine = self.__StandardLineDataToDict(singleRow) #对一行值进行标准化处理
                        if not dictLine: #如果处理不正确
                            pass
                        else:
                            nQuarter = 0 #设置财务季度
                            strIncome = singleRow[self.__dictConvertFields['EachIncome']['Pos']]#获取表示每股收益的字符串
                            for n in range(4): #判断季度值
                                if strIncome.find(self.__listQuarter[n]) != -1:#如果出现季度值
                                    nQuarter = (n+1)
                                    break
                            dictLine['Quarter'] = nQuarter
                            self.__listData.append(dictLine) #将处理好的数据存储到列表中
                return True
        except:
            return False

        
    def __SetConvertFieldsPos(self, listTitle:list=[]):
        """
        :DESCRIPTION:
            设置数据数据中转换字段的对应位置
        :PARAMS:
            listTitle:标题字符串列表
        :RETURNS:
            True:成功 False:失败
        """
        if not listTitle:
            return False
        nLen = len(listTitle) #获取列数
        for key in self.__dictConvertFields.keys(): #处理每一个键值
            strDescribe = self.__dictConvertFields[key]['Describe']
            for n in range(nLen):
                if strDescribe == listTitle[n]: #如果匹配
                    self.__dictConvertFields[key]['Pos'] = n #设置对应的位置
                    break #中断循环
        return True

            
    def __StandardLineDataToDict(self, listData:list=[]):
        """
        :DESCRIPTION:
            将分隔后的数据进行标准化处理，将结果转换为字典形式
        :param listData: 分隔后的数据列表
        :RETURNS: 转换后的字典数据/出现错误返回None
        """
        if not listData:
            return None
        dictReturn = dict()
        for key in self.__dictConvertFields.keys():#循环处理每一个键值
            dictDef = self.__dictConvertFields[key] #获取该键的定义
            unitValue = listData[dictDef['Pos']]  #分隔后对应该键位置的单元值
            errorValue = dictDef['ErrorValue'] #获取错误值
            defaultValue = dictDef['DefaultValue'] #获取缺省值
            if defaultValue == None:#如果没有定义缺省值,则表示严格该值不能缺省
                if unitValue == errorValue:#如果该单元值出现设定的错误值
                    return None
            try:
                if dictDef['Type'] == 'STR':#如果定义是字符串类型,则直接赋值
                    dictReturn[key] = unitValue
                else:#其它定义类型情况
                    if isinstance(unitValue, str):#如果单元格的值是str型
                        if unitValue.find('--') != -1:#出现错误值,则使用缺省值替换
                            dictReturn[key] = defaultValue
                        else:#没有出现错误值
                            unitValue = unitValue.strip()#去掉空格,方便类型转换
                            if dictDef['Type'] == 'DATE':#定义为日期格式
                                dictReturn[key] = TypeConvert.DateLongToSqlStr(int(unitValue))#将字符串格式的整数转换成YYYY-MM-DD字符串日期
                            elif dictDef['Type'] == 'FLOAT':#定义为浮点类型
                                dictReturn[key] = TypeConvert.StrTopNumToFloat(unitValue, defaultValue)
                            elif dictDef['Type'] == 'INT':#定义为整数类型
                                dictReturn[key] = TypeConvert.StrTopNumToFloat(unitValue, defaultValue)
                            else:#错误定义,则使用缺省值
                                dictReturn[key] = defaultValue
                    else:#单元格的值不是str类型
                        if dictDef['Type'] == 'DATE':#定义为日期格式
                            dictReturn[key] = TypeConvert.DateLongToSqlStr(unitValue)#将整数转换成YYYY-MM-DD字符串日期
                        else:#其它情况
                            dictReturn[key] = unitValue
            except Exception as e:
                print(e)
                print(listData)
                input("出现以上错误值")
                return None
        if self.__bXlsxFile: #如果是XLSX文件,则需要对股票代码进行特殊化处理
            dictReturn['StockCode'] = dictReturn['StockCode'][2:-1]
        return dictReturn
    

    def GetData(self):
        """
        :DESCRIPTION:
            获取类中保存的全部数据
        :PARAMS:
            nothing
        :RETURNS:
            None:没有数据 List:只要有数据返回
        """
        if not self.__listData:
            return None
        else:
            listReturn = []
            for item in self.__listData:
                listReturn.append(item)
            return listReturn
        

    def SearchData(self, value, strKey:str='', bLike:bool=False):
        """
        :DESCRIPTION:
            根据输入的键值查找符合要求的全部数据
        :PARAMS:
            value: 匹配的值
            strkey: 值对应的键名
            bLike: 如果是字符串值是否模糊匹配
        :RETURNS:
            None:没有数据或没有匹配的数据 List:有匹配的数据
        """
        if not value: #如果没有传入值
            return None
        if not strKey: #如果没有传入键名
            return None
        else:
            if not isinstance(strKey, str): #如果传入的键名不是字符串类型
                print( TypeConvert.StrToColorStr("KydTdxData---->>SearchData 传入的键名类型错误!", 'alarm'))
                return None
            else:
                if strKey not in self.__listFields: #如果传入的键名不在规范内
                    print( TypeConvert.StrToColorStr("KydTdxData---->>SearchData 传入非法键名!", 'alarm'))
                    return None
        if bLike: #如果是模糊查找,则需要判断value值必须为字符串类型
            if not isinstance(value, str):
                print( TypeConvert.StrToColorStr("KydTdxData---->>SearchData 模糊查找值必须为字符串类型!", 'alarm'))
                return None
        try:
            listReturn = []
            for item in self.__listData: #循环查找
                bAdd = False #添加标志位
                if bLike: #如果是模糊查找
                    if item[strKey].find(value) != -1:#如果查找到
                        bAdd = True
                else:
                    if item[strKey] == value:
                        bAdd = True
                if bAdd:
                    dictAdd = {}
                    for key in self.__listFields: #执行浅拷贝
                        dictAdd[key] = item[key]
                    listReturn.append(dictAdd)
            return listReturn
        except:
            return None


    def GetFields(self):
        """
        :DESCRIPTION:
            获取类中的键名列表
        :PARAMS:
            nothing
        :RETURNS:
            None:没有数据 List:键名列表
        """
        if not self.__listFields:
            return None
        listReturn = []
        for key in self.__listFields:
            listReturn.append(key)
        return listReturn


    def __SplitStrLine(self, strLine:str=''):
        """
        :DESCRIPTION:
            分隔一行字符串数据并去掉无关字符
        :PARAMS:
            strLine: 一行字符串数据
        :RETURNS:
            None:错误或无参数 List:数据列表
        """
        if not strLine:
            return None
        try:
            if not isinstance(strLine, str):
                return None
            else:
                listStr = strLine.split('\t') #分隔
                for item in listStr: #去掉无用的字符
                    item.strip()
                return listStr
        except:
            return None


    def __ReadTXT(self):
        """
        :DESCRIPTION:
            读取txt文件中的数据,不需要检查参数,上位函数已经检查
        :PARAMS:
            nothing
        :RETURNS:
            True:读取成功 False:读取失败
        """
        try:
            print("正在读取文件%s 需要一些时间,请等待..........." % self.__strFileName)
            with open(file=self.__strFilePath, mode='r', encoding=self.__strEncoding) as fp: #读取文件
                listLine = fp.readlines() #读取全部的行
                if (not listLine) or (len(listLine) <= 2): #如果读取错误
                    print( TypeConvert.StrToColorStr("KydTdxData---->>ReadFile 读取文件失败!", 'alarm') )
                    return False
                else:
                    listTitle = self.__SplitStrLine(listLine[0]) #获取表头
                    if not listTitle: #如果转换错误
                        print( TypeConvert.StrToColorStr("KydTdxData---->>ReadFile 表头内容错误", 'alarm') )
                        return False
                    if not self.__SetConvertFieldsPos(listTitle): #设置转换字符对应位置
                        print( TypeConvert.StrToColorStr("KydTdxData---->>ReadFile 设置转换字符位置不成功!", 'alarm') )
                        return False
                    else:
                        nLen = len(listLine) #获取数据长度
                        dictEstaDate = self.__dictConvertFields['EstaDate'] #获取上市日期的转换定义
                        for pos in range(1, (nLen-1)): #循环处理每一条数据,不要表头和表尾
                            curLine = listLine[pos] #获取当前行的字符串
                            listValue = self.__SplitStrLine(curLine) #分隔获取数值
                            if not listValue: #如果分隔数据出错,跳过这行数据
                                continue
                            else:
                                if not self.__bNotEsta: #如果不添加未上市股票
                                    if listValue[dictEstaDate['Pos']].find(dictEstaDate['ErrorValue']) != -1: #如果未上市
                                        continue #跳过这行数据
                                dictValue = self.__StandardLineDataToDict(listValue) #标准化数据
                                if not dictValue: #如果标准化错误
                                    pass
                                else:
                                    nQuarter = 0 #财务季度值
                                    strIncome = listValue[self.__dictConvertFields['EachIncome']['Pos']]#获取表示每股收益的字符串
                                    for n in range(4): #判断季度值
                                        if strIncome.find(self.__listQuarter[n]) != -1:#如果出现季度值
                                            nQuarter = (n+1)
                                            break
                                    dictValue['Quarter'] = nQuarter
                                    self.__listData.append(dictValue) #将数据添加进数据列表中
            print("读取文件%s 完毕!" % self.__strFileName)
            print("共读取 %d 条数据!" % len(self.__listData))
            return True
        except:
            print( TypeConvert.StrToColorStr("KydTdxData---->>ReadTxt 读取文件出现错误,请检查编码是否正确!", 'alarm'))
            return False
